使用Flask-Mail提供电子邮件支持

表6-1 Flask-Mail SMTP服务器的配置

配置 默认值 说明
MAIL_SERVER localhost 邮件服务器主机名或IP地址
MAIL_PORT 25 电子邮件服务器的端口
MAIL_USE_TLS False 启用传输层安全(TLS)协议
MAIL_USE_SSL False 启用安全套接层(SSL)协议
MAIL_USENAME None 邮件账户的用户名
MAIL_PASSWORD None 邮件账户的授权码
  1. hello.py中配置Flask-Mail使用QQ邮箱:
1
2
3
4
5
6
7
8
import os
# ...
app.config['MAIL_SERVER'] = 'smtp.qq.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')

注意:千万不要把账号密令直接写在脚本中,应该从环境变量中导入。

  1. 初始化Flask-Mail:
1
2
3
4
from flask_mail import Mail
# ...
mail = Mail(app)
  1. 在环境变量中定义MAIL_USERNAMEMAIL_PASSWORD
1
2
(venv) $ export MAIL_USERNAME='12345678@qq.com'
(venv) $ export MAIL_PASSWORD='qwertyuiop'

此时可以使用echo命令打印出来检查一下:

1
2
3
4
(venv) $ echo $MAIL_USERNAME
>>> '12345678@qq.com'
(venv) $ echo $MAIL_PASSWORD
>>> 'qwertyuiop'

定义环境变量后,此时在同一个终端中执行python hello.py shell命令,便能获取到想要的环境变量。

注意:如果定义好环境变量后把终端关闭再重新打开,那么此时是没有上次定义的环境变量的。所以需要在同一个终端中执行。

如何让QQ邮箱开启SMTP功能,可以参考flask-mail常见的邮箱配置问题解决

在Python shell中发送电子邮件

在上一个终端中,发送电子邮件:

1
2
3
4
5
6
7
8
(venv) $ python hello.py shell
>>> from flask_mail import Message
>>> from hello import mail
>>> msg = Message('test subject', sender='12345678@qq.com', recipients=['87654321@qq.com'])
>>> msg.body = 'test body'
>>> msg.html = '<p>test body</p>'
>>> with app.app_context():
··· mail.send(msg)

注意:在Flask-Mail中的send()函数使用current_app(程序上下文),因此在shell中发送邮件,需要激活程序上下文。

在程序中集成发送电子邮件功能

  1. 定义用于发邮件的函数,使hello.py支持电子邮件:
1
2
3
4
5
6
7
8
9
10
11
12
13
from flask_mail import Message
# ...
# 给邮件标题添加一个前缀
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
# 定义发件人
app.config['FLASKY_MAIL_SENDER'] = '12345678@qq.com'
def send_email(recipients, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject, sender=app.config['FLASKY_MAIL_SENDER'], recipients=[recilients])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
mail.send(msg)

邮件有纯文本.txt,也有HTML文本.html,客户端显示哪个,取决于邮件客户端的设置。

拓展:为什么send()函数中需要**kwargs**kwargs的作用是什么?
答:(1)因为我们不确定模板中需要什么变量参数,在此例中,模板需要的时user这个参数,但是如果换成别的模版,它不仅需要user参数,也需要其他一些参数(如datatime等,关键取决于模板设计成什么样),此时如果我们死死地把send_email()函数写成send_email(recipient, subject, template, user),那么就失去了灵活性,当换成其他模版时,datatime参数也就无法传入了,因此send()函数需要**kwargs
(2)**kwargs的作用是:当我们不知道需要往函数中传入多少个关键字参数或者想以字典的形式作为参数时,我们可以用**kwargs,这样我们就可以根据实际情况需要,往函数中传入特定个数的参数(数量使具体情况而定)。

  1. 结合视图函数发送电子邮件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# ...
# 定义收件人为Flasky的管理员
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
# ...
@app.route('/', methods=['GET', 'POST']
def index():
form = NameForm()
if form.validate_on_submit():
session['name'] = form.name.data
# 查找数据库中是否有该用户名,如果没有,就向数据库中添加新用户,
# 因为前面已经设置了SQLALCHEMY_COMMIT_ON_TEARDOWN,所以当请求结束时会自动提交事务
user = User.query.filter_by(user_name=form.name.data).first()
if user is None:
# 插入数据库
user = User(user_name=form.name.data)
db.session.add(user)
# 用于模板中判断是显示Pleased to meet you 还是 Happy to see you again
session['known'] = Flase
# 如果收件人不为空,则发送邮件
if app.config['FLASKY_ADMIN']:
send_email(app.config['FLASKY_ADMIN'], 'New User', 'mail/new_user', user=user)
else:
session['konwn'] = True
return redirect(url_for('index')
return render_template('index.html', form=form, name=session.get('name'), known=session.get('known', Flase))

此时我们也需要定义环境变量FLASKY_ADMIN
(venv) $ export FLASKY_ADMIN='12345678@qq.com'

template文件夹下的模板文件mail/new_user有两个,分别为new_user.txtnew_user.html

new_user.txt如下:

1
There has a new user that name is {{ user.user_name }}

new_user.html如下:

1
<h1>There has a new user that name is {{ user.user_name }}</h1>

异步发送电子邮件

为了避免处理请求过程中不必要的延迟,我们可以把发送电子邮件的函数移到后台线程中处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from threading import Thread
# ...
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(recipients, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject, sender=app.config['FLASKY_MAIL_SENDER'], recipients=[recilients])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr

注意:由于在不同线程中执行mail.send()函数,就如前面在Python shell中发送电子邮件章节中提到,send()函数需要程序current_app(程序上下文)中执行,因此需要在执行send()函数的线程中使用app.app_context()人工创建current_app

当需要发送大量电子邮件时,使用专门发送电子邮件的作业(如Celery任务队列)处理更合适。